// process_traits_win32.cpp

#include <iostream>

#define BOOST_IOSTREAMS_WINDOWS

#include <boost/iostreams/categories.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>

#include <boost/shared_ptr.hpp>
#include <boost/mpl/map.hpp>
#include <boost/mpl/int.hpp>
#include <boost/mpl/at.hpp>
#include <boost/mpl/equal_to.hpp>
#include <boost/type_traits/is_same.hpp>

#include "process_traits_win32.hpp"
#include "popen.hpp"

namespace io = boost::iostreams;
namespace pd = process::detail;
namespace mpl = boost::mpl;

namespace {
	class pipe_handle_wrapper {
	public:
		pipe_handle_wrapper(HANDLE h): handle_(h) {}
		~pipe_handle_wrapper(void) {
			CloseHandle(handle_);
		}
		operator HANDLE() {
			return handle_;
		}
	private:
		HANDLE handle_;
	};

	class pipe_source {
	public:
	    typedef char        char_type;
		typedef io::source_tag  category;

		pipe_source(boost::shared_ptr<pipe_handle_wrapper> pipe): pipe_(pipe) {}

		std::streamsize read(char* s, std::streamsize n) {
			DWORD bytes;
			if (!pipe_) return -1;
			if (!ReadFile(*pipe_, s, n, &bytes, NULL)) {
				pipe_ = boost::shared_ptr<pipe_handle_wrapper>();
				return std::char_traits<char>::eof();
			}
			return bytes;
		}
	private:
		boost::shared_ptr<pipe_handle_wrapper> pipe_;
	};

	class pipe_sink {
	public:
		typedef char      char_type;
		typedef io::sink_tag  category;

		pipe_sink (boost::shared_ptr<pipe_handle_wrapper> pipe): pipe_(pipe) {}

		std::streamsize write(const char* s, std::streamsize n) {
			DWORD bytes;
			WriteFile(*pipe_, s, n, &bytes, NULL) ;
			return bytes;
		}
	private:
		boost::shared_ptr<pipe_handle_wrapper> pipe_;
	};

	std::pair<HANDLE, HANDLE> create_pipe(mpl::true_) {
		SECURITY_ATTRIBUTES sa;
		sa.nLength = sizeof(SECURITY_ATTRIBUTES);
		sa.bInheritHandle = TRUE;
		sa.lpSecurityDescriptor = NULL;

		HANDLE read, write;
		if (!CreatePipe(&read, &write, &sa, 0)) throw std::runtime_error("error in call CreatePipe");

		return std::make_pair(write, read);
	}

	std::pair<HANDLE, HANDLE> create_pipe(mpl::false_) {
		std::pair<HANDLE, HANDLE> handle_pair = create_pipe(mpl::true_());
		return std::make_pair(handle_pair.second, handle_pair.first);
	}

	void set_pipe_to_startup_info(STARTUPINFO *start_info, HANDLE pipe, mpl::int_<STD_INPUT_HANDLE>) {
		start_info->hStdInput = pipe;
	}

	void set_pipe_to_startup_info(STARTUPINFO *start_info, HANDLE pipe, mpl::int_<STD_OUTPUT_HANDLE>) {
		start_info->hStdOutput = pipe;
	}

	void set_pipe_to_startup_info(STARTUPINFO *start_info, HANDLE pipe, mpl::int_<STD_ERROR_HANDLE>) {
		start_info->hStdError = pipe;
	}


	template <DWORD std_handle>
	class prepare_std_handle {
	public:
		template <typename T> class type2type {};

		typedef mpl::map<
			mpl::pair<mpl::int_<STD_INPUT_HANDLE>, pipe_sink>,
			mpl::pair<mpl::int_<STD_OUTPUT_HANDLE>, pipe_source>,
			mpl::pair<mpl::int_<STD_ERROR_HANDLE>, pipe_source> > stream_map_type;

		typedef typename mpl::at<stream_map_type, mpl::int_<std_handle> >::type pipe_wrapper_type;
		typedef typename io::stream<pipe_wrapper_type> stream_type;

		prepare_std_handle(STARTUPINFO *start_info) {
			std::pair<HANDLE, HANDLE> handle_pair = create_pipe(boost::is_same<pipe_wrapper_type, pipe_sink>::type());
			set_pipe_to_startup_info(start_info, handle_pair.second, mpl::int_<std_handle>::type());
			for_close_ = handle_pair.second;

			HANDLE curproc = GetCurrentProcess();
			HANDLE t;
			if (!DuplicateHandle(curproc, handle_pair.first, curproc, &t, 0, FALSE, DUPLICATE_SAME_ACCESS))
				throw std::runtime_error("error in call DuplicateHandle");
			CloseHandle(handle_pair.first);
			use_handle_ = boost::shared_ptr<pipe_handle_wrapper>(new pipe_handle_wrapper(t));
		}

		~prepare_std_handle(void) {
			CloseHandle(for_close_);
		}

		std::auto_ptr<stream_type> stream(void) {
			return std::auto_ptr<stream_type>(new stream_type(use_handle_));
		}
	private:
		HANDLE for_close_;
		boost::shared_ptr<pipe_handle_wrapper> use_handle_;
	};


	class prepare_union_stdout_and_stderr_type {
	public:
		prepare_union_stdout_and_stderr_type(STARTUPINFO *start_info) {
			HANDLE curproc = GetCurrentProcess();
			if (!DuplicateHandle(curproc, start_info->hStdOutput, curproc, &for_close_, 0, TRUE, DUPLICATE_SAME_ACCESS))
				throw std::runtime_error("error in call DuplicateHandle");
			start_info->hStdError = for_close_;
		}

		~prepare_union_stdout_and_stderr_type(void) {
			CloseHandle(for_close_);
		}
	private:
		HANDLE for_close_;
	};


	typedef prepare_std_handle<STD_OUTPUT_HANDLE> prepare_std_output_type;
	typedef prepare_std_handle<STD_ERROR_HANDLE> prepare_std_error_type;
	typedef prepare_std_handle<STD_INPUT_HANDLE> prepare_std_input_type;

}

pd::process_traits_win32::process_traits_win32(const char *cmd, int flags) {
	std::auto_ptr<prepare_union_stdout_and_stderr_type> prepare_union_stdout_and_stderr;
	std::auto_ptr<prepare_std_output_type> prepare_std_output;
	std::auto_ptr<prepare_std_error_type> prepare_std_error;
	std::auto_ptr<prepare_std_input_type> prepare_std_input;


	STARTUPINFO start_info;
	memset(&start_info, 0, sizeof(STARTUPINFO));
	start_info.dwFlags = STARTF_USESTDHANDLES;

	if (flags & process::popen::with_stdin) {
		prepare_std_input = std::auto_ptr<prepare_std_input_type>(new prepare_std_input_type(&start_info));
		to_ = prepare_std_input->stream();
	}


	if (flags & process::popen::with_union_stdout_and_stderr) {
		prepare_std_output = std::auto_ptr<prepare_std_output_type>(new prepare_std_output_type(&start_info));
		from_ = prepare_std_output->stream();

		prepare_union_stdout_and_stderr = std::auto_ptr<prepare_union_stdout_and_stderr_type>(new prepare_union_stdout_and_stderr_type(&start_info));
	}
	else {
		if (flags & process::popen::with_stdout) {
			prepare_std_output = std::auto_ptr<prepare_std_output_type>(new prepare_std_output_type(&start_info));
			from_ = prepare_std_output->stream();
		}

		if (flags & process::popen::with_stderr) {
			prepare_std_error = std::auto_ptr<prepare_std_error_type>(new prepare_std_error_type(&start_info));
			error_ = prepare_std_error->stream();
		}
	}

	PROCESS_INFORMATION proc_info;
	memset(&proc_info, 0, sizeof(PROCESS_INFORMATION));


	if (!CreateProcess(NULL, const_cast<char *>(cmd), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &start_info, &proc_info))
		throw std::runtime_error("error in call CreateProcess");

	CloseHandle(proc_info.hThread);
	process_ = proc_info.hProcess;
}

int pd::process_traits_win32::poll(void) const {
	DWORD exit_code;
	if (GetExitCodeProcess(process_, &exit_code)) return exit_code == STILL_ACTIVE ? -1 : exit_code;

	throw std::runtime_error("error in call GetExitCodeProcess");
}

int pd::process_traits_win32::wait(void) {
	if (WAIT_FAILED == WaitForSingleObject(process_, INFINITE)) throw std::runtime_error("error in call WaitForSingleObject");

	return poll();
}

pd::process_traits_win32::~process_traits_win32(void) {
	CloseHandle(process_);
}
